package com.ayu.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.ayu.shortlink.project.common.biz.user.UserContext;
import com.ayu.shortlink.project.common.cache.CaffeineCacheFactory;
import com.ayu.shortlink.project.common.cache.CommonCacheService;
import com.ayu.shortlink.project.common.constant.RedisKeyConstant;
import com.ayu.shortlink.project.common.convention.exception.ClientException;
import com.ayu.shortlink.project.common.convention.exception.ServiceException;
import com.ayu.shortlink.project.common.enums.BucketTokenKeyEnum;
import com.ayu.shortlink.project.common.enums.validDateTypeEnum;
import com.ayu.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.ayu.shortlink.project.dao.entiry.ShortLinkDO;
import com.ayu.shortlink.project.dao.entiry.ShortLinkGotoDO;
import com.ayu.shortlink.project.dao.mapper.ShortLinkGotoMapper;
import com.ayu.shortlink.project.dao.mapper.ShortLinkMapper;
import com.ayu.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.ayu.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.ayu.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.ayu.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.ayu.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.ayu.shortlink.project.dto.resp.*;
import com.ayu.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.ayu.shortlink.project.mq.producer.ShortLinkStatsUnionSaveProducer;
import com.ayu.shortlink.project.service.ShortLinkService;
import com.ayu.shortlink.project.util.CaffeineTokenBucketRateLimiter;
import com.ayu.shortlink.project.util.HashUtil;
import com.ayu.shortlink.project.util.LinkUtil;
import com.ayu.shortlink.project.util.TimeStampUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static com.ayu.shortlink.project.common.constant.ShortLinkConstant.UV_HYPERLOGLOG_KEY;

@Service
@Slf4j
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {
    private final RBloomFilter<String> shortLinkCreatCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final RedissonClient redissonClient;
    private final StringRedisTemplate stringRedisTemplate;
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;
    private final ShortLinkStatsUnionSaveProducer shortLinkStatsUnionSaveProducer;
    private final Lock lock = new ReentrantLock();
    private final CaffeineTokenBucketRateLimiter caffeineTokenBucketRateLimiter;
    private final TimeStampUtil timeStampUtil = new TimeStampUtil();

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO create(ShortLinkCreateReqDTO requestParam) {
        //验证是否为白名单域名
        verificationWhitelist(requestParam.getOriginUrl());

        String shortUri = generateSuffix2(requestParam);
        String fullShortUrl = createShortLinkDefaultDomain + "/" + shortUri;

        ShortLinkDO linkDO = ShortLinkDO
                .builder()
                .gid(requestParam.getGid())
                .shortUri(shortUri)
                .fullShortUrl(fullShortUrl)
                .describe(requestParam.getDescribe())
                .originUrl(requestParam.getOriginUrl())
                .domain(createShortLinkDefaultDomain)
                .createdType(requestParam.getCreatedType())
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .build();
        ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO
                .builder()
                .gid(requestParam.getGid())
                .fullShortUrl(fullShortUrl)
                .build();
        try {
            baseMapper.insert(linkDO);
            shortLinkGotoMapper.insert(linkGotoDO);
            //将当前shortUri加入布隆过滤器
            shortLinkCreatCachePenetrationBloomFilter.add(fullShortUrl);
        }catch (DuplicateKeyException exception){
            //能进入到这里说明可能是Redis宕机造成布隆过滤器数据丢失
            if(!shortLinkCreatCachePenetrationBloomFilter.contains(fullShortUrl)){
                shortLinkCreatCachePenetrationBloomFilter.add(fullShortUrl);
            }
            log.warn("短链接:{} 重复入库",fullShortUrl);
            throw new ServiceException("短链接重复生成");

        }

        ShortLinkCreateRespDTO respDTO = BeanUtil.toBean(linkDO, ShortLinkCreateRespDTO.class);
        respDTO.setFullShortUrl("http://"+respDTO.getFullShortUrl());
        //进行缓存预热
        stringRedisTemplate.opsForValue()
                .set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY,fullShortUrl),requestParam.getOriginUrl(), LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()),TimeUnit.MICROSECONDS);

        //并查询是否该缓存存在空值 如果存在则删除该空值key
        String nullSignal = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(nullSignal)){
            stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl));
        }
        return respDTO;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO create2(ShortLinkCreateReqDTO requestParam) {
        //验证是否为白名单域名
        verificationWhitelist(requestParam.getOriginUrl());

        String shortUri = generateSuffix3(requestParam);
        String fullShortUrl = createShortLinkDefaultDomain + "/" + shortUri;

        ShortLinkDO linkDO = ShortLinkDO
                .builder()
                .gid(requestParam.getGid())
                .shortUri(shortUri)
                .fullShortUrl(fullShortUrl)
                .describe(requestParam.getDescribe())
                .originUrl(requestParam.getOriginUrl())
                .domain(createShortLinkDefaultDomain)
                .createdType(requestParam.getCreatedType())
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .build();
        ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO
                .builder()
                .gid(requestParam.getGid())
                .fullShortUrl(fullShortUrl)
                .build();
        try {
            baseMapper.insert(linkDO);
            shortLinkGotoMapper.insert(linkGotoDO);
            //将当前shortUri加入布隆过滤器
            shortLinkCreatCachePenetrationBloomFilter.add(fullShortUrl);
        }catch (DuplicateKeyException exception){
            //能进入到这里说明可能是Redis宕机造成布隆过滤器数据丢失
            if(!shortLinkCreatCachePenetrationBloomFilter.contains(fullShortUrl)){
                shortLinkCreatCachePenetrationBloomFilter.add(fullShortUrl);
            }
            log.warn("短链接:{} 重复入库",fullShortUrl);
            throw new ServiceException("短链接重复生成");

        }

        ShortLinkCreateRespDTO respDTO = BeanUtil.toBean(linkDO, ShortLinkCreateRespDTO.class);
        respDTO.setFullShortUrl("http://"+respDTO.getFullShortUrl());
        //进行缓存预热
        stringRedisTemplate.opsForValue()
                .set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY,fullShortUrl),requestParam.getOriginUrl(), LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()),TimeUnit.MICROSECONDS);

        //并查询是否该缓存存在空值 如果存在则删除该空值key
        String nullSignal = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(nullSignal)){
            stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl));
        }
        return respDTO;
    }

    @Override
    public ShortLinkBatchCreateRespDTO batchCreat(ShortLinkBatchCreateReqDTO requestParam) {
        //获取原始短链接和描述集合
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();

        List<ShortLinkBaseInfoRespDTO> baseLinkInfos = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));

            try {
                ShortLinkCreateRespDTO creatRespDTO = create(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(creatRespDTO.getFullShortUrl())
                        .describe(describes.get(i))
                        .originUrl(originUrls.get(i))
                        .build();
                baseLinkInfos.add(linkBaseInfoRespDTO);
            }catch (Throwable throwable){
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(baseLinkInfos.size())
                .baseLinkInfos(baseLinkInfos)
                .build();
    }

    @Override
    public IPage<ShortLinkPageRespDTO> getPage(ShortLinkPageReqDTO requestParam) {
        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);

        return resultPage.convert(each -> BeanUtil.toBean(each, ShortLinkPageRespDTO.class));
    }

    @Override
    public List<ShortLinkGroupCountRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid,count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag",0)
                .eq("del_time",0L)
                .groupBy("gid");

        List<Map<String, Object>> maps = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(maps,ShortLinkGroupCountRespDTO.class);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        verificationWhitelist(requestParam.getOriginUrl());
        ShortLinkDO shortLinkDO = lambdaQuery()
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0)
                .one();
        if(shortLinkDO == null){
            throw new ClientException("短链接记录不存在");
        }

        if (Objects.equals(shortLinkDO.getGid(), requestParam.getGid())) {
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), validDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null);
            ShortLinkDO LinkDO = ShortLinkDO.builder()
                    .domain(shortLinkDO.getDomain())
                    .fullShortUrl(shortLinkDO.getFullShortUrl())
                    .shortUri(shortLinkDO.getShortUri())
                    .favicon(shortLinkDO.getFavicon())
                    .createdType(shortLinkDO.getCreatedType())
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .build();
            baseMapper.update(LinkDO, updateWrapper);
        } else {
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(RedisKeyConstant.LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock rLock = readWriteLock.writeLock();
            //阻塞获取写锁
            rLock.lock();
            try {
                LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, shortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLinkDO.setDelFlag(1);
                baseMapper.update(delShortLinkDO, linkUpdateWrapper);
                ShortLinkDO LinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(shortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(shortLinkDO.getShortUri())
                        .enableStatus(shortLinkDO.getEnableStatus())
                        .totalPv(shortLinkDO.getTotalPv())
                        .totalUv(shortLinkDO.getTotalUv())
                        .totalUip(shortLinkDO.getTotalUip())
                        .fullShortUrl(shortLinkDO.getFullShortUrl())
                        .favicon(getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(LinkDO);
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, shortLinkDO.getGid());
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                shortLinkGotoMapper.delete(linkGotoQueryWrapper);
                shortLinkGotoDO.setGid(requestParam.getGid());
                shortLinkGotoMapper.insert(shortLinkGotoDO);
            } finally {
                rLock.unlock();
            }
        }
        //若修改短链接的有效期或者类型就删除掉redis的go-to缓存
        if(!Objects.equals(shortLinkDO.getValidDate(),requestParam.getValidDate())
                || !Objects.equals(shortLinkDO.getValidDateType(),requestParam.getValidDateType())
                || !Objects.equals(shortLinkDO.getOriginUrl(),requestParam.getOriginUrl())){
            // 进行延迟双删 通过Redis的延迟队列进行延迟双删
            stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, shortLinkDO.getFullShortUrl()));
            RBlockingQueue<Object> delayDeleteQueue = redissonClient.getBlockingQueue("DELAY_DELETE_QUEUE");
            RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(delayDeleteQueue);
            delayedQueue.offer(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, shortLinkDO.getFullShortUrl()), 5, TimeUnit.SECONDS);

            // 删除本地缓存
            CommonCacheService<String, String> shortLinkCache = CaffeineCacheFactory.getShortLinkCache();
            shortLinkCache.remove(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, shortLinkDO.getFullShortUrl()));

            //再判断它修改后的过期时间或类型是否有效，若有效则把not-go-to缓存删掉
            if(Objects.equals(requestParam.getValidDateType(),validDateTypeEnum.PERMANENT.type) || requestParam.getValidDate().after(new Date())){
                //进行延迟双删
                stringRedisTemplate.delete(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY, shortLinkDO.getFullShortUrl()));
                delayedQueue.offer(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY, shortLinkDO.getFullShortUrl()), 5, TimeUnit.SECONDS);
            }

        }
    }

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl = serverName + serverPort + "/" + shortUri;

        //先用布隆过滤器验证一下
        if(!shortLinkCreatCachePenetrationBloomFilter.contains(fullShortUrl)){
            ((HttpServletResponse)response).sendRedirect("/page/notfound");
            return;
        }

        String originalUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(originalUrl)){
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(shortLinkStatsRecordDTO);
            ((HttpServletResponse)response).sendRedirect(originalUrl);
            return;
        }

        //再验证该空值url是否
        String nullSignal = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(nullSignal)){
            ((HttpServletResponse)response).sendRedirect("/page/notfound");
            return;
        }

        //缓存查不到 就会去查找数据库，为了避免大量请求同时访问，造成缓存击穿，所以使用分布式锁
        RLock lock = redissonClient.getLock(String.format(RedisKeyConstant.LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));

        lock.lock();
        try {
            //双重判断 因为第一次到的会添加到缓存 所以往后查这里一般都会有数据 加快效率
            originalUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
            if(StrUtil.isNotBlank(originalUrl)){
                ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                shortLinkStats(shortLinkStatsRecordDTO);
                ((HttpServletResponse)response).sendRedirect(originalUrl);
                return;
            }
            //双重判断空缓存 防止缓存穿透
            nullSignal = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl));
            if(StrUtil.isNotBlank(nullSignal)){
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                return;
            }
            //这块是为了防止数据库查询超时，导致存错空值
            LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper;
            ShortLinkGotoDO shortLinkGotoDO;
            try {
                queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
            }catch (Throwable throwable){
                log.warn("数据库查询存在异常");
                throwable.printStackTrace();
                return;
            }
            if(shortLinkGotoDO == null){
                //标注该url为空 过期时间30分钟
                stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl),"-",30, TimeUnit.MINUTES);
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                //TODO 严谨来说此处需要风控
                return;
            }

            ShortLinkDO shortLinkDO = lambdaQuery()
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .one();
            //判断是否存在以及有效期是否有效 无效跳转到/page/notfound
            if(shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))){
                //标注该url为空 过期时间30分钟
                stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl),"-",30, TimeUnit.MINUTES);
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                return;
            }
            //统计
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(shortLinkStatsRecordDTO);

            //添加到缓存 并跳转
            stringRedisTemplate.opsForValue().
                    set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY,fullShortUrl),shortLinkDO.getOriginUrl(),LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),TimeUnit.MICROSECONDS);
            ((HttpServletResponse)response).sendRedirect(shortLinkDO.getOriginUrl());
        }finally {
            lock.unlock();;
        }
    }

    @SneakyThrows
    @Override
    public void restoreUrl2(String shortUri, ServletRequest request, ServletResponse response) {
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl = serverName + serverPort + "/" + shortUri;

        //现实场景下 改原url的可能性很小 所以可以用本地缓存
        CommonCacheService<String, String> shortLinkCache = CaffeineCacheFactory.getShortLinkCache();
        String originalUrl = shortLinkCache.get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(originalUrl)){
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(shortLinkStatsRecordDTO);
            ((HttpServletResponse)response).sendRedirect(originalUrl);
            return;
        }

        //先用布隆过滤器验证一下
        if(!shortLinkCreatCachePenetrationBloomFilter.contains(fullShortUrl)){
            ((HttpServletResponse)response).sendRedirect("/page/notfound");
            return;
        }

        //再查Redis是否有url
        originalUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(originalUrl)){
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(shortLinkStatsRecordDTO);
            ((HttpServletResponse)response).sendRedirect(originalUrl);
            return;
        }

        //再验证该空值url是否
        String nullSignal = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(nullSignal)){
            ((HttpServletResponse)response).sendRedirect("/page/notfound");
            return;
        }

        //缓存查不到 就会去查找数据库，为了避免大量请求同时访问，造成缓存击穿，所以使用分布式锁
        RLock lock = redissonClient.getLock(String.format(RedisKeyConstant.LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));

        lock.lock();
        try {
            //双重判断 因为第一次到的会添加到缓存 所以往后查这里一般都会有数据 加快效率
            originalUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
            if(StrUtil.isNotBlank(originalUrl)){
                ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                shortLinkStats(shortLinkStatsRecordDTO);
                ((HttpServletResponse)response).sendRedirect(originalUrl);
                return;
            }
            //双重判断空缓存 防止缓存穿透
            nullSignal = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl));
            if(StrUtil.isNotBlank(nullSignal)){
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                return;
            }
            //这块是为了防止数据库查询超时，导致存错空值
            LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper;
            ShortLinkGotoDO shortLinkGotoDO;
            try {
                queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
            }catch (Throwable throwable){
                log.warn("数据库查询存在异常");
                throwable.printStackTrace();
                return;
            }
            if(shortLinkGotoDO == null){
                //标注该url为空 过期时间30分钟
                stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl),"-",30, TimeUnit.MINUTES);
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                //TODO 严谨来说此处需要风控
                return;
            }

            ShortLinkDO shortLinkDO = lambdaQuery()
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .one();
            //判断是否存在以及有效期是否有效 无效跳转到/page/notfound
            if(shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))){
                //标注该url为空 过期时间30分钟
                stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl),"-",30, TimeUnit.MINUTES);
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                return;
            }
            //统计
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(shortLinkStatsRecordDTO);

            //添加到Redis缓存 并跳转
            stringRedisTemplate.opsForValue().
                    set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY,fullShortUrl),shortLinkDO.getOriginUrl(),LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),TimeUnit.MICROSECONDS);
            ((HttpServletResponse)response).sendRedirect(shortLinkDO.getOriginUrl());
        }finally {
            lock.unlock();;
        }
    }

    @Override
    public void shortLinkStats(ShortLinkStatsRecordDTO statsRecord) {
        Map<String,String> producerMap = new HashMap<>();
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
        shortLinkStatsSaveProducer.send(producerMap);
    }

    private void shortLinkStats2(ShortLinkStatsRecordDTO shortLinkStatsRecordDTO) {
        shortLinkStatsUnionSaveProducer.sendMessage(shortLinkStatsRecordDTO);
    }

    @SneakyThrows
    @Override
    public void restoreUrl3(String shortUri, ServletRequest request, ServletResponse response) {
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        //String fullShortUrl = serverName + serverPort + "/" + shortUri;
        String fullShortUrl = createShortLinkDefaultDomain + "/" + shortUri;

        // 先尝试走本地缓存
        CommonCacheService<String, String> shortLinkCache = CaffeineCacheFactory.getShortLinkCache();
        String originalUrl = shortLinkCache.get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(originalUrl)){
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser2(fullShortUrl, originalUrl, request, response);
            shortLinkStats2(shortLinkStatsRecordDTO);
            ((HttpServletResponse)response).sendRedirect(originalUrl);
            return;
        }

        // 先用布隆过滤器验证一下 并验证Redis是否连接异常
        try {
            if(!shortLinkCreatCachePenetrationBloomFilter.contains(fullShortUrl)){
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                return;
            }
        }catch (RedisConnectionFailureException exception){
            // TODO 需要进行日志报警和通知
            log.error("Redis连接异常，请立马检查");
            // 然后进行降级方案 尝试拿两个令牌(先拿两个吧)
            String key = BucketTokenKeyEnum.SHORT_LINK_GOTO_TOKEN_KEY.getKey();
            if (!caffeineTokenBucketRateLimiter.tryAcquire(key, 2)){
                throw new ServiceException("当前服务器繁忙，请稍后再重试");
            }

            LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
            ShortLinkDO shortLinkDO = lambdaQuery()
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, shortLinkGotoDO.getFullShortUrl())
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .one();
            if (shortLinkDO != null){
                // 进行重定向
                ((HttpServletResponse)response).sendRedirect(shortLinkDO.getOriginUrl());
                return;
            }
            throw new ServiceException("当前服务器繁忙，请稍后再重试");
        }


        //再查Redis是否有url
        originalUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(originalUrl)){
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser2(fullShortUrl, originalUrl, request, response);
            shortLinkStats2(shortLinkStatsRecordDTO);
            ((HttpServletResponse)response).sendRedirect(originalUrl);
            return;
        }

        //再验证该空值url是否
        String nullSignal = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(nullSignal)){
            ((HttpServletResponse)response).sendRedirect("/page/notfound");
            return;
        }

        //缓存查不到 就会去查找数据库，为了避免大量请求同时访问，造成缓存击穿，所以使用分布式锁
        RLock lock = redissonClient.getLock(String.format(RedisKeyConstant.LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));

        lock.lock();
        try {
            //双重判断 因为第一次到的会添加到缓存 所以往后查这里一般都会有数据 加快效率
            originalUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
            if(StrUtil.isNotBlank(originalUrl)){
                ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser2(fullShortUrl, originalUrl, request, response);
                shortLinkStats2(shortLinkStatsRecordDTO);
                ((HttpServletResponse)response).sendRedirect(originalUrl);
                return;
            }
            //双重判断空缓存 防止缓存穿透
            nullSignal = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl));
            if(StrUtil.isNotBlank(nullSignal)){
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                return;
            }
            //这块是为了防止数据库查询超时，导致存错空值
            LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper;
            ShortLinkGotoDO shortLinkGotoDO;
            try {
                queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
            }catch (Throwable throwable){
                log.warn("数据库查询存在异常");
                throwable.printStackTrace();
                return;
            }
            if(shortLinkGotoDO == null){
                //标注该url为空 过期时间30分钟
                stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl),"-",30, TimeUnit.MINUTES);
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                //TODO 严谨来说此处需要风控
                return;
            }

            ShortLinkDO shortLinkDO = lambdaQuery()
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .one();
            //判断是否存在以及有效期是否有效 无效跳转到/page/notfound
            if(shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))){
                //标注该url为空 过期时间30分钟
                stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl),"-",30, TimeUnit.MINUTES);
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                return;
            }
            // 统计(MQ异步)
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser2(fullShortUrl, shortLinkDO.getOriginUrl(), request, response);
            shortLinkStats2(shortLinkStatsRecordDTO);

            //添加到Redis缓存 并跳转
            stringRedisTemplate.opsForValue().
                    set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY,fullShortUrl),shortLinkDO.getOriginUrl(),LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),TimeUnit.MICROSECONDS);
            ((HttpServletResponse)response).sendRedirect(shortLinkDO.getOriginUrl());
        }finally {
            lock.unlock();;
        }
    }

    @SneakyThrows
    @Override
    public void restoreUrl4(String shortUri, ServletRequest request, ServletResponse response) {
        CommonCacheService<String, Integer> tokenBucketCache = CaffeineCacheFactory.getTokenBucketCache();
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        //String fullShortUrl = serverName + serverPort + "/" + shortUri;
        String fullShortUrl = createShortLinkDefaultDomain + "/" + shortUri;

        // 先尝试走本地缓存
        CommonCacheService<String, String> shortLinkCache = CaffeineCacheFactory.getShortLinkCache();
        String originalUrl = shortLinkCache.get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(originalUrl)){
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser2(fullShortUrl, originalUrl, request, response);
            shortLinkStats2(shortLinkStatsRecordDTO);
            ((HttpServletResponse)response).sendRedirect(originalUrl);
            return;
        }

        // 先用布隆过滤器验证一下 并验证Redis是否连接异常
        try {
            if(!shortLinkCreatCachePenetrationBloomFilter.contains(fullShortUrl)){
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                return;
            }
        }catch (RedisConnectionFailureException exception){
            // TODO 需要进行日志报警和通知
            log.error("Redis连接异常，请立马检查");
            // 然后进行降级方案 尝试拿两个令牌(先拿两个吧)
            String key = BucketTokenKeyEnum.SHORT_LINK_GOTO_TOKEN_KEY.getKey();
            if (!caffeineTokenBucketRateLimiter.tryAcquire(key, 2)){
                throw new ServiceException("当前服务器繁忙，请稍后再重试");
            }
            LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
            ShortLinkDO shortLinkDO = lambdaQuery()
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, shortLinkGotoDO.getFullShortUrl())
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .one();
            if (shortLinkDO != null){
                // 进行重定向
                ((HttpServletResponse)response).sendRedirect(shortLinkDO.getOriginUrl());
                return;
            }
            throw new ServiceException("当前服务器繁忙，请稍后再重试");
        }

        //再查Redis是否有url
        originalUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(originalUrl)){
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser2(fullShortUrl, originalUrl, request, response);
            shortLinkStats2(shortLinkStatsRecordDTO);
            ((HttpServletResponse)response).sendRedirect(originalUrl);
            return;
        }

        //再验证该空值url是否
        String nullSignal = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(nullSignal)){
            ((HttpServletResponse)response).sendRedirect("/page/notfound");
            return;
        }

        //缓存查不到 就会去查找数据库，为了避免大量请求同时访问，造成缓存击穿，所以本地锁
        lock.lock();
        try {
            //双重判断 因为第一次到的会添加到缓存 所以往后查这里一般都会有数据 加快效率
            originalUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
            if(StrUtil.isNotBlank(originalUrl)){
                ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser2(fullShortUrl, originalUrl, request, response);
                shortLinkStats2(shortLinkStatsRecordDTO);
                ((HttpServletResponse)response).sendRedirect(originalUrl);
                return;
            }
            //双重判断空缓存 防止缓存穿透
            nullSignal = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl));
            if(StrUtil.isNotBlank(nullSignal)){
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                return;
            }
            //这块是为了防止数据库查询超时，导致存错空值
            LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper;
            ShortLinkGotoDO shortLinkGotoDO;
            try {
                queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
            }catch (Throwable throwable){
                log.warn("数据库查询存在异常");
                throwable.printStackTrace();
                return;
            }
            if(shortLinkGotoDO == null){
                //标注该url为空 过期时间30分钟
                stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl),"-",30, TimeUnit.MINUTES);
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                //TODO 严谨来说此处需要风控
                return;
            }

            ShortLinkDO shortLinkDO = lambdaQuery()
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .one();
            //判断是否存在以及有效期是否有效 无效跳转到/page/notfound
            if(shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))){
                //标注该url为空 过期时间30分钟
                stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_NULL_KEY,fullShortUrl),"-",30, TimeUnit.MINUTES);
                ((HttpServletResponse)response).sendRedirect("/page/notfound");
                return;
            }
            // 统计(MQ异步)
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser2(fullShortUrl, shortLinkDO.getOriginUrl(), request, response);
            shortLinkStats2(shortLinkStatsRecordDTO);

            //添加到Redis缓存 并跳转
            stringRedisTemplate.opsForValue().
                    set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY,fullShortUrl),shortLinkDO.getOriginUrl(),LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),TimeUnit.MICROSECONDS);
            ((HttpServletResponse)response).sendRedirect(shortLinkDO.getOriginUrl());
        }finally {
            lock.unlock();;
        }
    }

    /**
     * 生成短URL的V1版本 (MurmurHash + UUID)
     * @param requestParam
     * @return
     */
    private String generateSuffix(ShortLinkCreateReqDTO requestParam){
        int customGeneratorCount = 0;
        String shortUri;
        while (true){
            if(customGeneratorCount>10){
                throw new ClientException("短链接频繁生成");
            }

            shortUri = HashUtil.MurmurHashToBase62(requestParam.getOriginUrl()+UUID.randomUUID());
            if(!shortLinkCreatCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain+"/"+shortUri)){
                return shortUri;
            }
            customGeneratorCount++;
        }
    }

    /**
     * 生成短URL的V2版本 (MurmurHash + userId + TimeStamp)
     * @param requestParam
     * @return
     */
    private String generateSuffix2(ShortLinkCreateReqDTO requestParam){
        int customGeneratorCount = 0;
        String shortUri = null;
        while (true){
            if(customGeneratorCount>10){
                throw new ClientException("短链接频繁生成");
            }

            Long nextId = null;
            try {
                nextId = timeStampUtil.nextId();
            }catch (Exception exception){
                // 走到这里意味着发生了时间回拨问题 那么就使用原来的方案 UUID
                shortUri = HashUtil.MurmurHashToBase62(requestParam.getOriginUrl()+UUID.randomUUID());
            }

            if (nextId != null){
                shortUri = HashUtil.MurmurHashToBase62(UserContext.getUserId()+requestParam.getOriginUrl()+nextId);
            }

            if(!shortLinkCreatCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain+"/"+shortUri)){
                return shortUri;
            }
            customGeneratorCount++;
        }
    }

    /**
     * 生成短URL的V3版本 (xxHash + userId + TimeStamp)
     * @param requestParam
     * @return
     */
    private String generateSuffix3(ShortLinkCreateReqDTO requestParam){
        int customGeneratorCount = 0;
        String shortUri = null;
        while (true){
            if(customGeneratorCount>10){
                throw new ClientException("短链接频繁生成");
            }
            // 优化：为了防止循环时多次System.currentTimeMillis()的结果都相同。
            Long nextId = null;
            try {
                nextId = timeStampUtil.nextId();
            }catch (Exception exception){
                // 走到这里意味着发生了时间回拨问题 那么就使用原来的方案 UUID
                shortUri = HashUtil.MurmurHashToBase62(requestParam.getOriginUrl()+UUID.randomUUID());
            }

            if (nextId != null){
                shortUri = HashUtil.MurmurHashToBase62(UserContext.getUserId()+requestParam.getOriginUrl()+nextId);
            }
            if(!shortLinkCreatCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain+"/"+shortUri)){
                return shortUri;
            }
            customGeneratorCount++;
        }
    }

    /**
     * 获取url网站的图标
     * @param url url地址
     * @return 网站图标的地址
     */
    @SneakyThrows
    private String getFavicon(String url){
        URL targetUrl = new URL(url);

        HttpURLConnection connection = (HttpURLConnection)targetUrl.openConnection();

        connection.setRequestMethod("GET");

        connection.connect();
        int responseCode = connection.getResponseCode();
        if(responseCode == HttpURLConnection.HTTP_OK){
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();

            if(faviconLink != null){
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

    /**
     * 封装短链接状态记录
     */
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.randomUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add(RedisKeyConstant.UV_COOKIES_KEY+fullShortUrl,uv.get());
        };
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long uvAdded = stringRedisTemplate.opsForSet().add(RedisKeyConstant.UV_COOKIES_KEY+fullShortUrl, each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        String remoteAddr = LinkUtil.getIp(((HttpServletRequest) request));
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        Long uipAdded = stringRedisTemplate.opsForSet().add(RedisKeyConstant.UV_COOKIES_KEY+fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .currentDate(new Date())
                .build();
    }

    /**
     * 封装短链接状态记录
     * 解决问题1：用Set统计UIP会造成的BigKey问题
     */
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser2(String fullShortUrl, String originUrl, ServletRequest request, ServletResponse response) {
        String remoteAddr = LinkUtil.getIp(((HttpServletRequest) request));
        RHyperLogLog<Object> hyperLogLog = redissonClient.getHyperLogLog(String.format(UV_HYPERLOGLOG_KEY, fullShortUrl));
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        hyperLogLog.add(remoteAddr);
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .originUrl(originUrl)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .currentDate(new Date())
                .build();
    }

    /**
     * 当enable为true时，验证原始链接是否有效
     * @param originUrl
     */
    private void verificationWhitelist(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if (enable == null || !enable) {
            return;
        }
        String domain = LinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if (!details.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }
}
